#include "basetextfind.h"

#include <QTimer>
#include <QPropertyAnimation>
#include <QWidget>
#include <QPaintEvent>
#include <QPainter>
#include <QTextBlock>

static QString expandRegExpReplacement(const QString &replaceText, const QStringList &capturedTexts)
{
    // handles \1 \\ \& & \t \n
    QString result;
    const int numCaptures = capturedTexts.size() - 1;
    for (int i = 0; i < replaceText.length(); ++i) {
        QChar c = replaceText.at(i);
        if (c == QLatin1Char('\\') && i < replaceText.length() - 1) {
            c = replaceText.at(++i);
            if (c == QLatin1Char('\\')) {
                result += QLatin1Char('\\');
            } else if (c == QLatin1Char('&')) {
                result += QLatin1Char('&');
            } else if (c == QLatin1Char('t')) {
                result += QLatin1Char('\t');
            } else if (c == QLatin1Char('n')) {
                result += QLatin1Char('\n');
            } else if (c.isDigit()) {
                int index = c.unicode()-'1';
                if (index < numCaptures) {
                    result += capturedTexts.at(index+1);
                } else {
                    result += QLatin1Char('\\');
                    result += c;
                }
            } else {
                result += QLatin1Char('\\');
                result += c;
            }
        } else if (c == QLatin1Char('&')) {
            result += capturedTexts.at(0);
        } else {
            result += c;
        }
    }
    return result;
}

static QString matchCaseReplacement_s(const QString &originalText, const QString &replaceText)
{
    if (originalText.isEmpty())
       return replaceText;

    //Now proceed with actual case matching
    bool firstIsUpperCase = originalText.at(0).isUpper();
    bool firstIsLowerCase = originalText.at(0).isLower();
    bool restIsLowerCase = true; // to be verified
    bool restIsUpperCase = true; // to be verified

    for (int i = 1; i < originalText.length(); ++i) {
        if (originalText.at(i).isUpper())
            restIsLowerCase = false;
        else if (originalText.at(i).isLower())
            restIsUpperCase = false;

        if (!restIsLowerCase && !restIsUpperCase)
            return replaceText; // mixed
    }

    if (restIsLowerCase) {
        QString res = replaceText.toLower();
        if (firstIsUpperCase)
            res.replace(0, 1, res.at(0).toUpper());
        return res;
    }

    if (restIsUpperCase) {
        QString res = replaceText.toUpper();
        if (firstIsLowerCase)
            res.replace(0, 1, res.at(0).toLower());
        return res;
    }

    return replaceText;         // mixed
}

static QString matchCaseReplacement(const QString &originalText, const QString &replaceText)
{
    if (originalText.isEmpty())
        return replaceText;

    //Find common prefix & suffix: these will be unaffected
    const int replaceTextLen = replaceText.length();
    const int originalTextLen = originalText.length();

    int prefixLen = 0;
    for (; prefixLen < replaceTextLen && prefixLen < originalTextLen; ++prefixLen)
        if (replaceText.at(prefixLen).toLower() != originalText.at(prefixLen).toLower())
            break;

    int suffixLen = 0;
    for (; suffixLen < replaceTextLen - prefixLen && suffixLen < originalTextLen - prefixLen; ++suffixLen)
        if (replaceText.at(replaceTextLen - 1 - suffixLen).toLower() != originalText.at(originalTextLen- 1 - suffixLen).toLower())
            break;

    //keep prefix and suffix, and do actual replacement on the 'middle' of the string
    return originalText.left(prefixLen)
            + matchCaseReplacement_s(originalText.mid(prefixLen, originalTextLen - prefixLen - suffixLen),
                                             replaceText.mid(prefixLen, replaceTextLen - prefixLen - suffixLen))
            + originalText.right(suffixLen);

}

static QTextDocument::FindFlags textDocumentFlagsForFindFlags(FindFlags flags)
{
    QTextDocument::FindFlags textDocFlags;
    if (flags & FindBackward)
        textDocFlags |= QTextDocument::FindBackward;
    if (flags & FindCaseSensitively)
        textDocFlags |= QTextDocument::FindCaseSensitively;
    if (flags & FindWholeWords)
        textDocFlags |= QTextDocument::FindWholeWords;
    return textDocFlags;
}

WrapIndicator::WrapIndicator(QWidget *parent):
    QWidget(parent),
    m_opacity(1.0)
{
    if (parent)
        setGeometry(QRect(parent->rect().center() - QPoint(25, 25),
                          parent->rect().center() + QPoint(25, 25)));
}

qreal WrapIndicator::opacity() const
{
    return m_opacity;
}
void WrapIndicator::setOpacity(qreal value)
{
    m_opacity = value; update();
}

void WrapIndicator::run()
{
    show();
    QTimer::singleShot(300, this, SLOT(runInternal()));
}

void WrapIndicator::paintEvent(QPaintEvent *)
{
    static QPixmap foreground(QLatin1String(":/find/images/wrapindicator.png"));
    QPainter p(this);
    p.setOpacity(m_opacity);
    p.drawPixmap(rect(), foreground);
}

void WrapIndicator::runInternal()
{
    QPropertyAnimation *anim = new QPropertyAnimation(this, "opacity", this);
    anim->setDuration(200);
    anim->setEndValue(0.);
    connect(anim, SIGNAL(finished()), this, SLOT(deleteLater()));
    anim->start(QAbstractAnimation::DeleteWhenStopped);
}

BaseTextFind::BaseTextFind(QPlainTextEdit *editor)
    : m_plaineditor(editor)
    , m_widget(editor)
    , m_findScopeVerticalBlockSelectionFirstColumn(-1)
    , m_findScopeVerticalBlockSelectionLastColumn(-1)
    , m_incrementalStartPos(-1)
    , m_incrementalWrappedState(false)
{
}


QTextCursor BaseTextFind::textCursor() const
{
    return m_plaineditor->textCursor();
}

void BaseTextFind::setTextCursor(const QTextCursor &cursor)
{
     m_plaineditor->setTextCursor(cursor);
}

QTextDocument *BaseTextFind::document() const
{
    return  m_plaineditor->document();
}

bool BaseTextFind::isReadOnly() const
{
    return m_plaineditor->isReadOnly();
}

void BaseTextFind::showWrapIndicator(QWidget *parent)
{
    (new WrapIndicator(parent))->run();
}

bool BaseTextFind::supportsReplace() const
{
    return !isReadOnly();
}
FindFlags BaseTextFind::supportedFindFlags() const
{
    return (FindFlags)(FindBackward | FindCaseSensitively
            | FindRegularExpression | FindWholeWords
            | FindPreserveCase);
}

void BaseTextFind::resetIncrementalSearch()
{
    m_incrementalStartPos = -1;
    m_incrementalWrappedState = false;
}

void BaseTextFind::clearResults()
{
    emit highlightAll(QString(), 0);
}

QString BaseTextFind::currentFindString() const
{
    QTextCursor cursor = textCursor();
    if (cursor.hasSelection() && cursor.block() != cursor.document()->findBlock(cursor.anchor()))
        return QString(); // multi block selection

    if (cursor.hasSelection())
        return cursor.selectedText();

    if (!cursor.atBlockEnd() && !cursor.hasSelection()) {
        cursor.movePosition(QTextCursor::StartOfWord);
        cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
        QString s = cursor.selectedText();
        foreach (QChar c, s) {
            if (!c.isLetterOrNumber() && c != QLatin1Char('_')) {
                s.clear();
                break;
            }
        }
        return s;
    }

    return QString();
}

QString BaseTextFind::completedFindString() const
{
    QTextCursor cursor = textCursor();
    cursor.setPosition(textCursor().selectionStart());
    cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
    return cursor.selectedText();
}

BaseTextFind::Result BaseTextFind::findIncremental(const QString &txt, FindFlags findFlags)
{
    QTextCursor cursor = textCursor();
    if (m_incrementalStartPos < 0)
        m_incrementalStartPos = cursor.selectionStart();
    cursor.setPosition(m_incrementalStartPos);
    bool wrapped = false;
    bool found =  find(txt, findFlags, cursor, &wrapped);
    if (wrapped != m_incrementalWrappedState && found) {
        m_incrementalWrappedState = wrapped;
        showWrapIndicator(m_widget);
    }
    if (found)
        emit highlightAll(txt, findFlags);
    else
        emit highlightAll(QString(), 0);
    return found ? Found : NotFound;
}

BaseTextFind::Result BaseTextFind::findStep(const QString &txt, FindFlags findFlags)
{
    bool wrapped = false;
    bool found = find(txt, findFlags, textCursor(), &wrapped);
    if (wrapped)
        showWrapIndicator(m_widget);
    if (found) {
        m_incrementalStartPos = textCursor().selectionStart();
        m_incrementalWrappedState = false;
    }
    return found ? Found : NotFound;
}

void BaseTextFind::replace(const QString &before, const QString &after,
                           FindFlags findFlags)
{
    QTextCursor cursor = replaceInternal(before, after, findFlags);
    setTextCursor(cursor);
}

QTextCursor BaseTextFind::replaceInternal(const QString &before, const QString &after,
                                          FindFlags findFlags)
{
    QTextCursor cursor = textCursor();
    bool usesRegExp = (findFlags & FindRegularExpression);
    bool preserveCase = (findFlags & FindPreserveCase);
    QRegExp regexp(before,
                   (findFlags & FindCaseSensitively) ? Qt::CaseSensitive : Qt::CaseInsensitive,
                   usesRegExp ? QRegExp::RegExp : QRegExp::FixedString);

    if (regexp.exactMatch(cursor.selectedText())) {
        QString realAfter;
        if (usesRegExp)
            realAfter = expandRegExpReplacement(after, regexp.capturedTexts());
        else if (preserveCase)
            realAfter = matchCaseReplacement(cursor.selectedText(), after);
        else
            realAfter = after;
        int start = cursor.selectionStart();
        cursor.insertText(realAfter);
        if ((findFlags&FindBackward) != 0)
            cursor.setPosition(start);
    }
    return cursor;
}

bool BaseTextFind::replaceStep(const QString &before, const QString &after,
    FindFlags findFlags)
{
    QTextCursor cursor = replaceInternal(before, after, findFlags);
    bool wrapped = false;
    bool found = find(before, findFlags, cursor, &wrapped);
    if (wrapped)
        showWrapIndicator(m_widget);
    return found;
}

int BaseTextFind::replaceAll(const QString &before, const QString &after,
    FindFlags findFlags)
{
    QTextCursor editCursor = textCursor();
    if (!m_findScopeStart.isNull())
        editCursor.setPosition(m_findScopeStart.position());
    else
        editCursor.movePosition(QTextCursor::Start);
    editCursor.beginEditBlock();
    int count = 0;
    bool usesRegExp = (findFlags & FindRegularExpression);
    bool preserveCase = (findFlags & FindPreserveCase);
    QRegExp regexp(before);
    regexp.setPatternSyntax(usesRegExp ? QRegExp::RegExp : QRegExp::FixedString);
    regexp.setCaseSensitivity((findFlags & FindCaseSensitively) ? Qt::CaseSensitive : Qt::CaseInsensitive);
    QTextCursor found = findOne(regexp, editCursor, textDocumentFlagsForFindFlags(findFlags));
    bool first = true;
    while (!found.isNull() && inScope(found.selectionStart(), found.selectionEnd())) {
        if (found == editCursor && !first) {
            if (editCursor.atEnd())
                break;
            // If the newly found QTextCursor is the same as recently edit one we have to move on,
            // otherwise we would run into an endless loop for some regular expressions
            // like ^ or \b.
            QTextCursor newPosCursor = editCursor;
            newPosCursor.movePosition(findFlags & FindBackward ?
                                          QTextCursor::PreviousCharacter :
                                          QTextCursor::NextCharacter);
            found = findOne(regexp, newPosCursor, textDocumentFlagsForFindFlags(findFlags));
            continue;
        }
        if (first)
            first = false;
        ++count;
        editCursor.setPosition(found.selectionStart());
        editCursor.setPosition(found.selectionEnd(), QTextCursor::KeepAnchor);
        regexp.exactMatch(found.selectedText());

        QString realAfter;
        if (usesRegExp)
            realAfter = expandRegExpReplacement(after, regexp.capturedTexts());
        else if (preserveCase)
            realAfter = matchCaseReplacement(found.selectedText(), after);
        else
            realAfter = after;
        editCursor.insertText(realAfter);
        found = findOne(regexp, editCursor, textDocumentFlagsForFindFlags(findFlags));
    }
    editCursor.endEditBlock();
    return count;
}

bool BaseTextFind::find(const QString &txt, FindFlags findFlags,
    QTextCursor start, bool *wrapped)
{
    if (txt.isEmpty()) {
        setTextCursor(start);
        return true;
    }
    QRegExp regexp(txt);
    regexp.setPatternSyntax((findFlags&FindRegularExpression) ? QRegExp::RegExp : QRegExp::FixedString);
    regexp.setCaseSensitivity((findFlags&FindCaseSensitively) ? Qt::CaseSensitive : Qt::CaseInsensitive);
    QTextCursor found = findOne(regexp, start, textDocumentFlagsForFindFlags(findFlags));
    if (wrapped)
        *wrapped = false;

    if (!m_findScopeStart.isNull()) {

        // scoped
        if (found.isNull() || !inScope(found.selectionStart(), found.selectionEnd())) {
            if ((findFlags&FindBackward) == 0)
                start.setPosition(m_findScopeStart.position());
            else
                start.setPosition(m_findScopeEnd.position());
            found = findOne(regexp, start, textDocumentFlagsForFindFlags(findFlags));
            if (found.isNull() || !inScope(found.selectionStart(), found.selectionEnd()))
                return false;
            if (wrapped)
                *wrapped = true;
        }
    } else {

        // entire document
        if (found.isNull()) {
            if ((findFlags&FindBackward) == 0)
                start.movePosition(QTextCursor::Start);
            else
                start.movePosition(QTextCursor::End);
            found = findOne(regexp, start, textDocumentFlagsForFindFlags(findFlags));
            if (found.isNull())
                return false;
            if (wrapped)
                *wrapped = true;
        }
    }
    if (!found.isNull())
        setTextCursor(found);
    return true;
}


// helper function. Works just like QTextDocument::find() but supports vertical block selection
QTextCursor BaseTextFind::findOne(const QRegExp &expr, const QTextCursor &from, QTextDocument::FindFlags options) const
{
    QTextCursor candidate = document()->find(expr, from, options);
    if (candidate.isNull())
        return candidate;

    if (m_findScopeVerticalBlockSelectionFirstColumn < 0)
        return candidate;
    forever {
        if (!inScope(candidate.selectionStart(), candidate.selectionEnd()))
            return candidate;
        bool inVerticalFindScope = false;
        QMetaObject::invokeMethod(m_plaineditor, "inFindScope", Qt::DirectConnection,
                                  Q_RETURN_ARG(bool, inVerticalFindScope),
                                  Q_ARG(QTextCursor, candidate));
        if (inVerticalFindScope)
            return candidate;

        QTextCursor newCandidate = document()->find(expr, candidate, options);
        if (newCandidate == candidate) {
            // When searching for regular expressions that match "zero length" strings (like ^ or \b)
            // we need to move away from the match before searching for the next one.
            candidate.movePosition(options & QTextDocument::FindBackward
                                   ? QTextCursor::PreviousCharacter
                                   : QTextCursor::NextCharacter);
            candidate = document()->find(expr, candidate, options);
        } else {
            candidate = newCandidate;
        }
    }
    return candidate;
}

bool BaseTextFind::inScope(int startPosition, int endPosition) const
{
    if (m_findScopeStart.isNull())
        return true;
    return (m_findScopeStart.position() <= startPosition
            && m_findScopeEnd.position() >= endPosition);
}

void BaseTextFind::defineFindScope()
{
    QTextCursor cursor = textCursor();
    if (cursor.hasSelection() && cursor.block() != cursor.document()->findBlock(cursor.anchor())) {
        m_findScopeStart = QTextCursor(document()->docHandle(), qMax(0, cursor.selectionStart()));
        m_findScopeEnd = QTextCursor(document()->docHandle(), cursor.selectionEnd());
        m_findScopeVerticalBlockSelectionFirstColumn = -1;
        m_findScopeVerticalBlockSelectionLastColumn = -1;

        if (m_plaineditor && m_plaineditor->metaObject()->indexOfProperty("verticalBlockSelectionFirstColumn") >= 0) {
            m_findScopeVerticalBlockSelectionFirstColumn
                    = m_plaineditor->property("verticalBlockSelectionFirstColumn").toInt();
            m_findScopeVerticalBlockSelectionLastColumn
                    = m_plaineditor->property("verticalBlockSelectionLastColumn").toInt();
        }

        emit findScopeChanged(m_findScopeStart, m_findScopeEnd,
                              m_findScopeVerticalBlockSelectionFirstColumn,
                              m_findScopeVerticalBlockSelectionLastColumn);
        cursor.setPosition(m_findScopeStart.position());
        setTextCursor(cursor);
    } else {
        clearFindScope();
    }
}

void BaseTextFind::clearFindScope()
{
    m_findScopeStart = QTextCursor();
    m_findScopeEnd = QTextCursor();
    m_findScopeVerticalBlockSelectionFirstColumn = -1;
    m_findScopeVerticalBlockSelectionLastColumn = -1;
    emit findScopeChanged(m_findScopeStart, m_findScopeEnd,
                          m_findScopeVerticalBlockSelectionFirstColumn,
                          m_findScopeVerticalBlockSelectionLastColumn);
}
